package gov.nist.healthcare.testframework.communication;

import gov.nist.healthcare.core.MalformedMessageException;
import gov.nist.healthcare.core.message.v3.HL7V3Message;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.MimeHeaders;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPConstants;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPHeaderElement;
import javax.xml.soap.SOAPMessage;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpVersion;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicHttpEntityEnclosingRequest;
import org.apache.http.params.BasicHttpParams;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlObject;
import org.w3c.dom.Attr;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * Holds an instance of an HTTP request which contains an SOAP Envelope as its
 * HTTP Entity. This Envelope contains an HL7V3Message. A HTTP Response is
 * comprised of three parts, the HTTPResponse itself, a SoapMessage, and a
 * HL7V3Response payload. This class can create a request in one of two ways.
 * When sending a request another server this class can build the outgoing
 * message by being provided the HL7V3Message. When receiving a request from a
 * client this class can parse an incoming HTTPRequest. All SOAP messages are
 * created/parsed using the SOAP 1.2 Protocol. All HTTP messages are encoded
 * using the HTTP 1_1 protocol.
 * 
 * @author Michael Ogata
 */
public class HSV3Request extends BasicHttpEntityEnclosingRequest implements
        HTTPSoapV3Container {

    protected HL7V3Message v3payload;
    protected static MessageFactory mf;
    protected SOAPMessage smessage;
    protected SOAPHeader sheader;

    // WSA Constants
    private static String WSA_NAMESPACE = "http://www.w3.org/2005/08/addressing";
    private static String WSA_PREFIX = "wsa";

    private static final int WSA_MIN_TO = 0;
    private static final int WSA_MAX_TO = 1;

    private static final int WSA_MIN_FROM = 0;
    private static final int WSA_MAX_FROM = 1;

    private static final int WSA_MIN_REPLY_TO = 0;
    private static final int WSA_MAX_REPLY_TO = 1;

    private static final int WSA_MIN_FAULT_TO = 0;
    private static final int WSA_MAX_FAULT_TO = 1;

    private static final int WSA_MIN_ACTION = 1;
    private static final int WSA_MAX_ACTION = 1;

    private static final int WSA_MIN_MESSAGEID = 0;
    private static final int WSA_MAX_MESSAGEID = 1;

    /**
     * Creates an empty HSV3 Request. This function will throw a SOAPException
     * if it is unable to create a new message from the message factory or if it
     * is unable to correctly manipulate the SOAP Header for this message
     * 
     * @throws SOAPException
     *         if it is unable to create a new message from the message factory
     *         or if it is unable to correctly manipulate the SOAP Header for
     *         this message
     */
    private HSV3Request() throws SOAPException {
        super("POST", "/", HttpVersion.HTTP_1_1);
        mf = MessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL);
        setHeader("Content-Type", "application/soap+xml");
        setHeader("Host", "");
        smessage = mf.createMessage();
        v3payload = null;
        buildDefaultHeader();
        smessage.saveChanges();
    }

    /**
     * Creates a HSV3Request with the provided HL7V3Message as its payload. This
     * constructor is typically used for building outgoing requests.
     * 
     * @param m
     * @param endpoint
     * @throws SOAPException
     *         if it is unable to create a new message from the message factory
     *         or if it is unable to correctly manipulate the SOAP Header for
     *         this message
     * @throws IOException
     *         if any IO error occurs
     * @throws SAXException
     *         if the XML factory cannot parse the HL7V3Message
     * @throws ParserConfigurationException
     */
    public HSV3Request(HL7V3Message m, String endpoint) throws SOAPException,
            IOException, SAXException, ParserConfigurationException {
        super("POST", endpoint, HttpVersion.HTTP_1_1);
        mf = MessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL);
        setParams(new BasicHttpParams());
        setHeader("Content-Type", "application/soap+xml");
        setHeader("Host", "");
        v3payload = m;
        smessage = mf.createMessage();

        // Load payload into XML document
        DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
        builderFactory.setNamespaceAware(true);
        InputStream stream = new ByteArrayInputStream(
                v3payload.getMessageAsString().getBytes());
        Document doc = builderFactory.newDocumentBuilder().parse(stream);

        // Generate SOAP SoapMessage

        SOAPBody sb = smessage.getSOAPBody();
        sb.addDocument(doc);

        // Generate Header nodes

        buildDefaultHeader();
        smessage.saveChanges();
    }

    /**
     * Builds an HSV3Request from an entity closing HTTP request. This
     * constructor is typically used for parsing incoming requests.
     * 
     * @param r
     * @throws SOAPException
     * @throws XmlException
     * @throws MalformedMessageException
     */
    public HSV3Request(HttpEntityEnclosingRequest r) throws SOAPException,
            XmlException, MalformedMessageException {
        // Copy HTTP header info
        super(r.getRequestLine());
        setHeaders(r.getAllHeaders());
        setEntity(r.getEntity());
        setParams(r.getParams());

        mf = MessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL);

        smessage = null;
        v3payload = null;

        HttpEntity entity = getEntity();

        if (entity != null) {
            // Build New Soap Message
            try {
                MimeHeaders mheaders = new MimeHeaders();
                smessage = mf.createMessage(mheaders, entity.getContent());
                // Build HL7V3 Message
                SOAPBody soapBody = smessage.getSOAPBody();
                XmlObject xml = XmlObject.Factory.parse(soapBody);
                XmlCursor cursor = xml.newCursor();
                // To the Body element
                cursor.toFirstChild();
                // To the root of the payload
                cursor.toFirstChild();
                XmlObject newXml = XmlObject.Factory.newInstance();
                XmlCursor newCursor = newXml.newCursor();
                newCursor.toNextToken();
                cursor.copyXml(newCursor);
                v3payload = new HL7V3Message(newXml);
                cursor.dispose();
            } catch (IllegalStateException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    /**
     * Builds the string representation of this HTTP Request. This function
     * builds this string from two sources. It uses the stored member headers to
     * construct the correct HTTP header information. It then constructs the
     * HTTP Entity from the stored SOAP Message.
     */
    public String getStringRepresentation() throws SOAPException, IOException,
            SAXException {
        String stringrep = "";
        Header[] headers;

        // Calculate the content length for this message
        removeHeaders("Content-Length");

        if (smessage != null) {
            ByteArrayOutputStream smout = new ByteArrayOutputStream();
            smessage.writeTo(smout);
            StringEntity e = new StringEntity(smout.toString());
            setHeader("Content-Length", Long.toString(e.getContentLength()));
        } else {
            setHeader("Content-Length", "0");
        }
        // Write Message to String
        headers = getAllHeaders();
        stringrep += getRequestLine().getMethod() + " "
                + getRequestLine().getUri() + " "
                + getRequestLine().getProtocolVersion().toString() + "\r\n";
        for (int i = 0; i < headers.length; ++i) {
            stringrep += headers[i].getName() + ": " + headers[i].getValue()
                    + "\r\n";
        }

        if (smessage != null) {
            OutputStream os = new ByteArrayOutputStream();
            smessage.writeTo(os);
            stringrep += "\r\n";
            stringrep += os.toString();
        }

        return stringrep;
    }

    public HL7V3Message getV3payload() {
        return this.v3payload;
    }

    /**
     * Checks the soap header for this message to determine if it adheres to the
     * following standards: Web Services Addressing 1.0 - Core, ITI TF-2x
     * Appendix V This function is not complete and as of now only checks for
     * cardinality constraints as indicated by the standards and for the
     * required presence of various mustUnderstand attributes.
     * 
     * @throws DOMException
     *         if the header is not well formed XML
     * @throws SOAPException
     *         if the header cannot be retrieved
     * @throws WSAException
     *         when a violation of the cardinality rules is detected
     */
    public boolean isWSAValid() throws DOMException, SOAPException,
            WSAException {
        boolean truthiness = true;
        NodeList wsa_nodes = null;
        SOAPHeader sh = null;
        String local_wsa_pre = "";
        String errorString = "";

        sh = smessage.getSOAPHeader();

        // Find the prefix for WSA for this header (note, it might not be
        // WSA_NAMESPACE_PRE, so we have to search for it)
        if (sh != null) {
            for (Iterator<String> cur_pre = sh.getNamespacePrefixes(); cur_pre.hasNext();) {
                String pre = cur_pre.next();
                if (sh.getNamespaceURI(pre).equals(WSA_NAMESPACE)) {
                    local_wsa_pre = pre;
                }
            }
        }
        if (sh != null
                && (wsa_nodes = sh.getElementsByTagNameNS(WSA_NAMESPACE, "*")).getLength() > 0) {
            List<Node> to = new ArrayList<Node>();
            List<Node> from = new ArrayList<Node>();
            List<Node> replyTo = new ArrayList<Node>();
            List<Node> faultTo = new ArrayList<Node>();
            List<Node> messageID = new ArrayList<Node>();
            List<Node> action = new ArrayList<Node>();

            // Count fields in header

            for (int i = 0; i < wsa_nodes.getLength(); ++i) {
                Node cur_node = wsa_nodes.item(i);
                String element_name = cur_node.getLocalName();
                if (element_name.equals("To")) {
                    to.add(cur_node.cloneNode(true));
                }
                if (element_name.equals("From")) {
                    from.add(cur_node.cloneNode(true));
                }
                if (element_name.equals("ReplyTo")) {
                    replyTo.add(cur_node.cloneNode(true));
                }
                if (element_name.equals("FaultTo")) {
                    faultTo.add(cur_node.cloneNode(true));
                }
                if (element_name.equals("MessageID")) {
                    messageID.add(cur_node.cloneNode(true));
                }
                if (element_name.equals("Action")) {
                    action.add(cur_node.cloneNode(true));
                }
            }

            if (to.size() < WSA_MIN_TO || to.size() > WSA_MAX_TO) {
                // ws-addr-core 3.1
                truthiness = false;
                errorString += "Cardinality Violation( ws-addr-core 3.1): A SOAP Header must contain 0..1 \"wsa:To\" elements\n";

            }

            if (from.size() < WSA_MIN_FROM || from.size() > WSA_MAX_FROM) {
                truthiness = false;
                errorString += "Cardinality Violation( ws-addr-core 3.1): A SOAP Header must contain 0..1 \"wsa:From\" elements\n";
            }

            if (replyTo.size() < WSA_MIN_REPLY_TO
                    || replyTo.size() > WSA_MAX_REPLY_TO) {
                truthiness = false;
                errorString += "Cardinality Violation( ws-addr-core 3.1): A SOAP Header must contain 0..1 \"wsa:ReplyTo\" elements\n";
            } else if (replyTo.size() != 0) {
                // IHE-WSA102
                NamedNodeMap rt_attrs = replyTo.get(0).getAttributes();
                boolean mu_correct = false;
                for (int i = 0; i < rt_attrs.getLength(); ++i) {
                    Attr cur_attr = (Attr) rt_attrs.item(i);

                    if (cur_attr.getName().equals(
                            local_wsa_pre + ":mustUnderstand")
                            && cur_attr.getValue().equals("1")) {
                        mu_correct = true;
                    }
                }
                if (!mu_correct) {
                    truthiness = false;
                    errorString += "Invalid Message Header( IHE-WSA102): \"wsa:ReplyTo\" must have attribute mustUnderstand=1\n";
                }
            }

            if (faultTo.size() < WSA_MIN_FAULT_TO
                    || faultTo.size() > WSA_MAX_FAULT_TO) {
                // ws-addr-core 3.1
                truthiness = false;
                errorString += "Cardinality Violation( ws-addr-core 3.1): A SOAP Header must contain 0..1 \"wsa:FaultTo\" elements\n";

            }

            if (messageID.size() < WSA_MIN_MESSAGEID
                    || messageID.size() > WSA_MAX_MESSAGEID) {
                // ws-addr-core 3.1
                truthiness = false;
                errorString += "Cardinality Violation( ws-addr-core 3.1): A SOAP Header must contain 0..1 \"wsa:MessageID\" elements\n";

            }

            if (action.size() < WSA_MIN_ACTION
            // ws-addr-core 3.1
                    || action.size() > WSA_MAX_ACTION) {
                truthiness = false;
                errorString += "Cardinality Violation( ws-addr-core 3.1): A SOAP Header must contain 0..1 \"wsa:Action\" elements\n";

            } else {
                // IHE-WSA102
                NamedNodeMap action_attrs = action.get(0).getAttributes();
                boolean mu_correct = false;

                for (int i = 0; i < action_attrs.getLength(); ++i) {
                    Attr cur_attr = (Attr) action_attrs.item(i);

                    if (cur_attr.getName().equals(
                            local_wsa_pre + ":mustUnderstand")
                            && cur_attr.getValue().equals("1")) {
                        mu_correct = true;
                    }
                }

                if (!mu_correct) {
                    truthiness = false;
                    errorString += "Invalid Message Header( IHE-WSA102): \"wsa:Action\" must have attribute mustUnderstand=1\n";
                }

                // IHE IHE-WSP208
                Pattern p = Pattern.compile("urn:ihe:[^:]*:\\d\\d\\d\\d:.*");
                Matcher m = p.matcher(action.get(0).getTextContent());
                if (!m.find()) {
                    truthiness = false;
                    errorString += "wsa:Action is malformed. Expected: \"urn:ihe:{Domain}:{Year}:{Transaction name} found "
                            + action.get(0).getTextContent();
                }

            }
        } else {
            truthiness = false;
            errorString += "Invalid Message Header( HL7-WSA100 ) HL7 WS MESSAGES MUST use the WS-Addressing framework for SOAP-based messaging using "
                    + WSA_NAMESPACE;
        }

        if (!truthiness) {
            throw new WSAException(errorString);
        }

        return truthiness;

    }

    /**
     * Sets the values of the required WSA header elements
     * 
     * @param to
     * @param from
     * @param replyto
     * @param action
     * @param messageid
     * @throws SOAPException
     */
    public void setWSAHeaderValues(String to, String from, String replyto,
            String action) throws SOAPException {
        SOAPHeader sh = smessage.getSOAPHeader();

        NodeList To = sh.getElementsByTagNameNS(WSA_NAMESPACE, "To");
        NodeList From = sh.getElementsByTagNameNS(WSA_NAMESPACE, "From");
        NodeList ReplyTo = sh.getElementsByTagNameNS(WSA_NAMESPACE, "ReplyTo");
        NodeList Action = sh.getElementsByTagNameNS(WSA_NAMESPACE, "Action");
        // NodeList MessageID = sh.getElementsByTagNameNS(WSA_NAMESPACE,
        // "MessageID");

        To.item(0).setTextContent(to);
        Action.item(0).setTextContent(action);
        // MessageID.item(0).setTextContent(messageid);

        // Set these element's first child (which should be Address) to
        // the appropriate values
        From.item(0).getFirstChild().setTextContent(from);
        ReplyTo.item(0).getFirstChild().setTextContent(replyto);

        smessage.saveChanges();
    }

    public void setWSATo(String value) throws SOAPException {
        SOAPHeader sh = smessage.getSOAPHeader();

        NodeList NList = sh.getElementsByTagNameNS(WSA_NAMESPACE, "To");

        // Check if there is already a MessageID Element

        if (NList.getLength() >= 1) {
            NList.item(0).setTextContent(value);
        } else {
            SOAPHeaderElement NewElement = sh.addHeaderElement(new QName(
                    WSA_NAMESPACE, "To", WSA_PREFIX));

            NewElement.setTextContent(value);
        }

        smessage.saveChanges();
    }

    public String getWSATo() throws SOAPException {
        String toreturn = "";
        SOAPHeader sh = smessage.getSOAPHeader();

        NodeList NList = sh.getElementsByTagNameNS(WSA_NAMESPACE, "To");
        // NodeList MessageIDList = sh.getElementsByTagName("MessageID");

        if (NList.getLength() >= 1) {
            toreturn = NList.item(0).getTextContent();
        }

        return toreturn;

    }

    public void setWSAFrom(String value) throws SOAPException {
        SOAPHeader sh = smessage.getSOAPHeader();

        NodeList NList = sh.getElementsByTagNameNS(WSA_NAMESPACE, "From");

        // I am worried about this working, its hard to test

        if (NList.getLength() >= 1) {
            SOAPElement a1 = (SOAPElement) NList.item(0).getFirstChild();
            if (a1 != null) {
                a1.setTextContent(value);
            } else {
                a1 = ((SOAPElement) NList.item(0)).addChildElement(new QName(
                        WSA_NAMESPACE, "Address", WSA_PREFIX));
                a1.setTextContent(value);
            }
        } else {
            SOAPHeaderElement NewElement = sh.addHeaderElement(new QName(
                    WSA_NAMESPACE, "From", WSA_PREFIX));
            SOAPElement a1 = NewElement.addChildElement(new QName(
                    WSA_NAMESPACE, "Address", WSA_PREFIX));

            a1.setTextContent(value);
        }

        smessage.saveChanges();
    }

    public String getWSAFrom() throws SOAPException {
        String toreturn = "";
        SOAPHeader sh = smessage.getSOAPHeader();

        NodeList NList = sh.getElementsByTagNameNS(WSA_NAMESPACE, "From");

        if (NList.getLength() >= 1) {
            Node parent_node = NList.item(0);
            NodeList children = parent_node.getChildNodes();

            for (int i = 0; i < children.getLength(); ++i) {
                String localname = children.item(i).getLocalName();
                if ((new String("Address").equals(localname))) {
                    toreturn = children.item(i).getTextContent();
                }
            }
        }

        return toreturn;

    }

    public void setWSAReplyTo(String value) throws SOAPException {
        SOAPHeader sh = smessage.getSOAPHeader();

        NodeList NList = sh.getElementsByTagNameNS(WSA_NAMESPACE, "ReplyTo");

        // I am worried about this working, its hard to test

        if (NList.getLength() >= 1) {
            SOAPElement a1 = (SOAPElement) NList.item(0).getFirstChild();
            if (a1 != null) {
                a1.setTextContent(value);
            } else {
                a1 = ((SOAPElement) NList.item(0)).addChildElement(new QName(
                        WSA_NAMESPACE, "Address", WSA_PREFIX));
                a1.setTextContent(value);
            }
        } else {
            SOAPHeaderElement NewElement = sh.addHeaderElement(new QName(
                    WSA_NAMESPACE, "ReplyTo", WSA_PREFIX));
            SOAPElement a1 = NewElement.addChildElement(new QName(
                    WSA_NAMESPACE, "Address", WSA_PREFIX));

            a1.setTextContent(value);
        }

        smessage.saveChanges();
    }

    public String getWSAReplyTo() throws SOAPException {
        String toreturn = "";
        SOAPHeader sh = smessage.getSOAPHeader();

        NodeList NList = sh.getElementsByTagNameNS(WSA_NAMESPACE, "ReplyTo");

        if (NList.getLength() >= 1) {
            Node parent_node = NList.item(0);
            NodeList children = parent_node.getChildNodes();

            for (int i = 0; i < children.getLength(); ++i) {
                String localname = children.item(i).getLocalName();
                if ((new String("Address").equals(localname))) {
                    toreturn = children.item(i).getTextContent();
                }
            }
        }

        return toreturn;

    }

    public void setWSAAction(String value) throws SOAPException {
        SOAPHeader sh = smessage.getSOAPHeader();

        NodeList NList = sh.getElementsByTagNameNS(WSA_NAMESPACE, "Action");

        // Check if there is already a MessageID Element

        if (NList.getLength() >= 1) {
            NList.item(0).setTextContent(value);
        } else {
            SOAPHeaderElement NewElement = sh.addHeaderElement(new QName(
                    WSA_NAMESPACE, "Action", WSA_PREFIX));

            NewElement.setTextContent(value);
        }

        smessage.saveChanges();
    }

    public String getWSAAction() throws SOAPException {
        String toreturn = "";
        SOAPHeader sh = smessage.getSOAPHeader();

        NodeList NList = sh.getElementsByTagNameNS(WSA_NAMESPACE, "Action");
        // NodeList MessageIDList = sh.getElementsByTagName("MessageID");

        if (NList.getLength() >= 1) {
            toreturn = NList.item(0).getTextContent();
        }

        return toreturn;

    }

    public void setWSAMessageID(String m) throws SOAPException {
        SOAPHeader sh = smessage.getSOAPHeader();

        NodeList MessageIDList = sh.getElementsByTagNameNS(WSA_NAMESPACE,
                "MessageID");

        // Check if there is already a MessageID Element

        if (MessageIDList.getLength() >= 1) {
            MessageIDList.item(0).setTextContent(m);
        } else {
            SOAPHeaderElement MIDElement = sh.addHeaderElement(new QName(
                    WSA_NAMESPACE, "MessageID", WSA_PREFIX));

            MIDElement.setTextContent(m);
        }

        smessage.saveChanges();

    }

    public String getWSAMessageID() throws SOAPException {
        String messageid = "";
        SOAPHeader sh = smessage.getSOAPHeader();

        NodeList MessageIDList = sh.getElementsByTagNameNS(WSA_NAMESPACE,
                "MessageID");
        // NodeList MessageIDList = sh.getElementsByTagName("MessageID");

        if (MessageIDList.getLength() >= 1) {
            messageid = MessageIDList.item(0).getTextContent();
        }

        return messageid;

    }

    /**
     * Adds the required elements to the SOAP header but DOES NOT set their
     * values. This function does not save the contents of the soap message. The
     * caller of this function must call smessage.saveChanges().
     * 
     * @throws SOAPException
     */
    private void buildDefaultHeader() throws SOAPException {
        SOAPHeader sh = smessage.getSOAPHeader();

        // Add mustUnderstand to WSA Header
        sh.addAttribute(new QName(WSA_NAMESPACE, "mustUnderstand", WSA_PREFIX),
                "1");
        // Get SOAP ENV prefix
        String env_prefix = sh.getPrefix();
        String env_namespace_uri = sh.getBaseURI();

        // Add required headers
        sh.addNamespaceDeclaration(WSA_PREFIX, WSA_NAMESPACE);
        sh.addHeaderElement(new QName(WSA_NAMESPACE, "To", WSA_PREFIX));
        SOAPHeaderElement rt = sh.addHeaderElement(new QName(WSA_NAMESPACE,
                "ReplyTo", WSA_PREFIX));
        SOAPHeaderElement a = sh.addHeaderElement(new QName(WSA_NAMESPACE,
                "Action", WSA_PREFIX));
        SOAPHeaderElement mid = sh.addHeaderElement(new QName(WSA_NAMESPACE,
                "MessageID", WSA_PREFIX));

        // Add mustUnderstand to appropriate elements
        rt.addAttribute((new QName(env_namespace_uri, "mustUnderstand",
                env_prefix)), "1");
        a.addAttribute((new QName(env_namespace_uri, "mustUnderstand",
                env_prefix)), "1");

        // Add Address sub-element to from and replyto

        SOAPElement a1 = rt.addChildElement(new QName(WSA_NAMESPACE, "Address",
                WSA_PREFIX));

    }

}
